/*
 *  Copyright 2021-2025 Michael T. Richter as Half-Baked Software
 *  This program is free software. It comes without any warranty, to the extent
 *  permitted by applicable law. You can redistribute it and/or modify it under
 *  the terms of the Do What The Fuck You Want To Public License, Version 2, as
 *  published by Sam Hocevar. See the file COPYING or http://www.wtfpl.net/ for
 *  more details.
 */

#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>

#include "evq.h"
#include "evq_impl.h"
#include "opalloc.h"

// helper declarations
static inline void insert_child(evq_queue_t p, evq_queue_t c);
static inline void insert_event(evq_queue_t q, evq_event_t e);
static inline void process_queue(evq_queue_t q, tick_ms current);
static inline void remove_event(evq_queue_t q, evq_event_t e);

// debugging statistics
static size_t debug_queues, debug_queue_entries, debug_callback_events, debug_coroutine_events, debug_events;
#define ADD_QUEUE() do { ++debug_queues; } while (false)
#define ADD_QUEUE_ENTRY() do { ++debug_queue_entries; } while (false)
#define DEL_QUEUE_ENTRY() do { --debug_queue_entries; } while (false)
#define ADD_FN_EVENT() do { ++debug_callback_events; ++debug_events; } while (false)
#define DEL_FN_EVENT() do { --debug_callback_events; --debug_events; } while (false)
#define ADD_CO_EVENT() do { ++debug_coroutine_events; ++debug_events; } while (false)
#define DEL_CO_EVENT() do { --debug_coroutine_events; --debug_events; } while (false)
#define DEL_EVENT(E) do { if (E->is_coroutine) DEL_CO_EVENT(); else DEL_FN_EVENT(); } while (false)

typedef struct _evq_queue_t
{
    // The internal structure of event queues is tree-structured:
    //
    // - each evq_queue_t has a pointer to the head of a NULL-terminated list of
    //   child event queues
    // - the list of child event queues is navigated through the sibling pointer
    // - the parent of each child is stored in the up pointer of said child
    evq_queue_t first_child, next_sibling, parent;

    // Events in each evq_queue_t are a singly linked list of evq_event_t items.
    evq_event_t head;

    // operations
    bool running;

    // idle function
    evq_idle idle;
} evq_queue_t_impl;

OP_HL_DECLARE_ALLOCATOR(evq_event_t_impl, 5, OP_DOUBLING_INDIVIDUAL)

// Snaps a child evq_queue_t c under the parent evq_queue_t p.
void evq_attach_queue(evq_queue_t p, evq_queue_t c)
{
    assert(p != NULL);
    assert(c != NULL);

    if (p != NULL && c != NULL)
    {
        insert_child(p, c);
    }
    c->parent = p;
    c->next_sibling = NULL;
}

void evq_cancel_event(evq_event_t e)
{
    assert(e != NULL);

    if (e->container) { remove_event(e->container, e); }
}

// Creates a manual event backed by a coroutine.
evq_event_t evq_create_co_event(delay_ms delay, delay_ms period, co_t co, void *data)
{
    assert(co != NULL);

    evq_event_t e;

    if ((e = allocate_evq_event_t_impl()))
    {
        evq_fill_co_event(e, delay, period, co, data);
        e->reusable = false;
    }

    assert(e != NULL);
    ADD_CO_EVENT();
    return e;
}

// Creates a manual event backed by a callback function.
evq_event_t evq_create_fn_event(delay_ms delay, delay_ms period, evq_callback cb, void *data)
{
    evq_event_t e;

    if ((e = allocate_evq_event_t_impl()))
    {
        evq_fill_fn_event(e, delay, period, cb, data);
        e->reusable = false;
    }

    assert(e != NULL);
    ADD_FN_EVENT();
    return e;
}

evq_queue_t evq_create_queue(void)
{
    evq_queue_t q;
    q = calloc(1, sizeof(evq_queue_t_impl));

    assert(q != NULL);

    ADD_QUEUE();
    return q;
}

void evq_destroy_event(evq_event_t e)
{
    assert(e != NULL);

    evq_cancel_event(e);

    if ((e)->in_use)
    {
        DEL_EVENT((e));
        deallocate_evq_event_t_impl(e);
    }
}

// Removes a child evq_queue_t from a parent chain.
void evq_detach_queue(evq_queue_t q)
{
    assert(q != NULL);

    if (q)
    {
        // if we don't have a parent we're already a detached root
        evq_queue_t parent = q->parent;
        if (parent)
        {
            evq_queue_t sibling = parent->first_child;

            // we're the first child, so point to the next
            if (sibling == q)
            {
                parent->first_child = q->next_sibling;
            }
            else
            {
                // otherwise find our place and pull out of the list
                while (sibling)
                {
                    if (sibling->next_sibling == q)
                    {
                        sibling->next_sibling = q->next_sibling;
                        break;
                    }
                    sibling = sibling->next_sibling;
                }
            }
        }
        // upon detachment we no longer have a parent nor siblings
        q->next_sibling = NULL;
        q->parent = NULL;
    }
}

void evq_fill_co_event(evq_event_t e, delay_ms delay, delay_ms period, co_t co, void *data)
{
    assert(e != NULL);
    assert(co != NULL);

    e->trigger_time = evq_get_tick() + delay;
    e->period = period;
    e->is_coroutine = true;
    e->co = co;
    e->data = data;
    e->chain = NULL;
    e->reusable = true;
}

void evq_fill_fn_event(evq_event_t e, delay_ms delay, delay_ms period, evq_callback cb, void *data)
{
    assert(e != NULL);

    e->trigger_time = evq_get_tick() + delay;
    e->period = period;
    e->is_coroutine = false;
    e->cb = cb;
    e->data = data;
    e->chain = NULL;
    e->reusable = true;
}

void evq_post_event(evq_queue_t q, evq_event_t e)
{
    assert(q != NULL);
    assert(e != NULL);

    if (q && e)
    {
        insert_event(q, e);
        e->container = q;
    }
}

delay_ms evq_query_time_remaining(evq_event_t e)
{
    assert(e != NULL);

    delay_ms tr = 0;

    tick_ms now = evq_get_tick();

    if (now < e->trigger_time)
    {
        tr = e->trigger_time - now;
    }

    return tr;
}

evq_event_t evq_queue_every(evq_queue_t q, evq_callback cb, void *data, delay_ms period)
{
    assert(q != NULL);
    assert(cb != NULL);

    evq_event_t e;

    if ((e = evq_create_fn_event(period, period, cb, data)))
    {
        evq_post_event(q, e);
    }

    assert(e != NULL);
    return (e);
}

evq_event_t evq_queue_in(evq_queue_t q, evq_callback cb, void *data, delay_ms delay)
{
    assert(q != NULL);
    assert(cb != NULL);

    evq_event_t e;

    if ((e = evq_create_fn_event(delay, 0, cb, data)))
    {
        evq_post_event(q, e);
    }

    assert(e != NULL);
    return (e);
}

evq_event_t evq_queue_now(evq_queue_t q, evq_callback cb, void *data)
{
    assert(q != NULL);
    assert(cb != NULL);

    evq_event_t e;

    if ((e = evq_create_fn_event(0, 0, cb, data)))
    {
        evq_post_event(q, e);
    }

    assert(e != NULL);
    return (e);
}

evq_event_t evq_resume_every(evq_queue_t q, co_t co, void *data, delay_ms period)
{
    assert(q != NULL);
    assert(co != NULL);

    evq_event_t e;

    if ((e = evq_create_co_event(period, period, co, data)))
    {
        evq_post_event(q, e);
    }

    assert(e != NULL);
    return (e);
}

evq_event_t evq_resume_in(evq_queue_t q, co_t co, void *data, delay_ms delay)
{
    assert(q != NULL);
    assert(co != NULL);

    evq_event_t e;

    if ((e = evq_create_co_event(delay, 0, co, data)))
    {
        evq_post_event(q, e);
    }

    assert(e != NULL);
    return (e);
}

evq_event_t evq_resume_now(evq_queue_t q, co_t co, void *data)
{
    assert(q != NULL);
    assert(co != NULL);

    evq_event_t e;

    if ((e = evq_create_co_event(0, 0, co, data)))
    {
        evq_post_event(q, e);
    }

    assert(e != NULL);
    return (e);
}

void evq_process(evq_queue_t q, evq_idle idle)
{
    assert(q != NULL);
    q->idle = idle;

    if (q)
    {
        q->running = true;

        while (q->running)
        {
            process_queue(q, evq_get_tick());
        }
    }
}

void evq_process_for(evq_queue_t q, delay_ms ms, evq_idle idle)
{
    assert(q != NULL);
    q->idle = idle;

    if (q)
    {
        bool running = q->running;

        tick_ms t = evq_get_tick() + ms;
        q->running = true;
        tick_ms c;

        while ((c = evq_get_tick()) < t)
        {
            process_queue(q, c);
        }

        q->running = running;
    }
}

void evq_process_one_tick(evq_queue_t q, evq_idle idle)
{
    assert(q != NULL);
    q->idle = idle;

    if (q)
    {
        bool running = q->running;

        q->running = true;
        process_queue(q, evq_get_tick());

        q->running = running;
    }
}

static inline void insert_child(evq_queue_t p, evq_queue_t c)
{
    evq_queue_t s = p->first_child;
    if (s != NULL)
    {
        while (s->next_sibling != NULL)
        {
            s = s->next_sibling;
        }
        s->next_sibling = c;
    }
    else
    {
        p->first_child = c;
    }
}

static inline void insert_event(evq_queue_t q, evq_event_t e)
{
    assert(q != NULL);
    assert(e != NULL);

    if (!q->head || (q->head->trigger_time > e->trigger_time))
    {
        // We're the first event in this evq_queue_t.
        e->chain = q->head;
        q->head = e;
        ADD_QUEUE_ENTRY();
    }
    else
    {
        evq_event_t c = q->head;

        do
        {
            evq_event_t n = c->chain;

            if (!n || n->trigger_time > e->trigger_time)
            {
                c->chain = e;
                e->chain = n;
                ADD_QUEUE_ENTRY();
                break;
            }

            c = n;
        }
        while (c);
    }
}

static inline void process_queue(evq_queue_t q, tick_ms current)
{
    assert(q != NULL);

    if (q)
    {
        evq_event_t e = q->head;

        while (q->running && e)
        {
            evq_event_t n = e->chain;

            if (e->trigger_time <= current)
            {
                bool is_coroutine = e->is_coroutine;
                evq_callback cb = e->cb;        // cb and co are the same piece of data
                co_t co = e->co;                // but it's more convenient this way
                void *data = e->data;
                evq_queue_t cq = e->container;

                if (e->period > 0)
                {
                    remove_event(cq, e);
                    e->trigger_time = current + e->period;
                    insert_event(cq, e);
                }
                else
                {
                    remove_event(cq, e);

                    if (!e->reusable)
                    {
                        DEL_EVENT(e);
                        deallocate_evq_event_t_impl(e);
                    }
                }

                if (is_coroutine)
                {
                    co_resume(co, data);
                }
                else
                {
                    cb(data);
                }
            }
            else
            {
                // All further events are not yet due.
                break;
            }

            e = n;
        }

        evq_queue_t child = q->first_child;
        while (child)
        {
            child->running = true;
            process_queue(child, current);
            child->running = false;
            child = child->next_sibling;
        }
    }

    do
    {
        if (q->idle)        { q->idle(); break; }
        else if (q->parent) { q = q->parent;    }
        else                { break;            }
    } while (q);
}

static inline void remove_event(evq_queue_t q, evq_event_t e)
{
    assert(q != NULL);
    assert(e != NULL);

    if (q->head)
    {
        if (q->head == e)
        {
            q->head = q->head->chain;
            DEL_QUEUE_ENTRY();
            return;
        }
        else
        {
            evq_event_t c = q->head;

            do
            {
                evq_event_t n = c->chain;

                if (!n)
                {
                    break;
                }
                else if (n == e)
                {
                    c->chain = n->chain;
                    DEL_QUEUE_ENTRY();
                    break;
                }

                c = n;
            }
            while (c);
        }
    }

    evq_queue_t child = q->first_child;
    while (child)
    {
        remove_event(child, e);
        child = child->next_sibling;
    }
}
